(ns #^{:doc "The core of the Clojure-Java DB interactions.
            Has helper functions to communicate with a database
            through a common handler and has support for translating
            Java Record classes to nested Clojure maps."}
  clj.jtacck.db.core
  (:import jvm.jtacck.db.AwesomeCoreHandler
	   [jvm.jtacck.db.records Record Status])
  (:require [clj.jtacck.cache :as cache]
	    [clj.jtacck.logging :as log]))

(def db-cache-lifetime (* 10 1000)) ; 10 seconds

(defn record-to-map
  "Takes in a Java object. If it is a jvm.jtacck.db.records.Record,
  it will be turned into an appropriate map from public field names
  to their values (which may be nested structures from other Records.
  Also converts Java collections (including arrays) into the
  corresponding Clojure collection."
  [obj]
  (letfn [(public-field? [f] (java.lang.reflect.Modifier/isPublic
			      (.getModifiers f)))
	  (get-field [f] (record-to-map (.get f obj))) ; Recurively convert to a map
	  (matches? [cls] (isa? (class obj) cls))]
    (cond
     (matches? Record)  (into {} (map (juxt (comp keyword (memfn getName)) get-field)
				      (filter public-field? (.getFields (class obj)))))
     (or (matches? (class (into-array [])))
	 (matches? java.util.ArrayList)) (vec (map record-to-map obj))
     (matches? java.util.Map)  (into {} (map (fn [[k v]]
					       [k (record-to-map v)]) obj))
     (matches? java.util.Set)  (into #{} (map record-to-map obj))
     (matches? java.util.Collection)  (map record-to-map obj)
     :else obj)))

(defn start-db-connection
  "Attempts to create a new connection to the database
  with the given username, password, and database address.
  The address is in the format: <server>:<port>/<databasename>.
  Returns true on success, false otherwise."
  [username password address]
  (AwesomeCoreHandler/createConnection username password address))

(defn stop-db-connection
  "Returns true if the connection to the database has been
  closed. Otherwise, returns false."
  []
  (try
    (AwesomeCoreHandler/closeConnection)
    (catch IllegalStateException e
      false)))

(defn get-user-info
  "Returns a user information map if the given username/password
  combination is valid, nil otherwise.
  Throws an IllegalStateException if no connection to the database
  is already established."
  [username password]
  (log/timing-info "get-user-info: "
   (record-to-map (AwesomeCoreHandler/getUserInfo username password))))

(defn get-all-shipping-info
  "Returns a coll of shipping information maps.
  Throws an IllegalStateException if no connection to the database
  is already established.

  Note: This is cached with a lifetime of db-cache-lifetime."
  []
  (log/timing-info
   "get-all-shipping-info: "
   (cache/cache-or-compute
    ::all-shipping-info
    (record-to-map (AwesomeCoreHandler/getAllShippingInformation))
    db-cache-lifetime)))

(defn get-all-client-sites
  "Returns a coll of client site maps.
  Throws an IllegalStateException if no connection to the database
  is already established.

  Note: This is cached with a lifetime of db-cache-lifetime."
  []
  (log/timing-info
   "get-all-client-sites: "
   (cache/cache-or-compute
    ::all-client-sites
    (record-to-map (AwesomeCoreHandler/getAllClientSites))
    db-cache-lifetime)))

(defn get-all-users
  "Returns a coll of user info maps.
  Throws an IllegalStateException if no connection to the database
  is already established.

  Note: This is cached with a lifetime of db-cache-lifetime."
  []
  (log/timing-info
   "get-all-users: "
   (cache/cache-or-compute
    ::all-users
    (record-to-map (AwesomeCoreHandler/getAllUsers))
    db-cache-lifetime)))

(defn get-all-hospitals
  "Returns a coll of hospital info maps.
  Throws an IllegalStateException if no connection to the database
  is already established.

  Note: This is cached with a lifetime of db-cache-lifetime."
  []
  (log/timing-info
   "get-all-hospitals: "
   (cache/cache-or-compute
    ::all-hospitals
    (record-to-map (AwesomeCoreHandler/getAllHospitals))
    db-cache-lifetime)))

(defn get-user-types
  "Returns a coll of user-type records.
  Throws an IllegalStateException if no connection to the database
  is already established.

  Note: This is cached with a lifetime of db-cache-lifetime."
  []
  (log/timing-info
   "get-user-types: "
   (cache/cache-or-compute
    ::user-types
    (record-to-map (AwesomeCoreHandler/getAllUserTypes)))))

(defn get-couriers
  "Returns a coll of courier records.
  Throws an IllegalStateException if no connection to the database
  is already established.

  Note: This is cached with a lifetime of db-cache-lifetime."
  []
  (log/timing-info
   "get-couriers: "
   (cache/cache-or-compute
    ::couriers
    (record-to-map (AwesomeCoreHandler/getAllCouriers)))))

(defn get-routes
  "Returns a map from courier usernames to their route list.
  Throws an IllegalStateException if no connection to the database
  is already established.

  Note: This is cached with a lifetime of db-cache-lifetime."
  []
  (into {}
	(for [{route :route
	       {:keys [username]} :user} (get-couriers)]
	  [username route])))

(defn get-client-checklist
  "Returns the checklist for the given client-site-id.
  Throws an IllegalStateException if no connection to the database
  is already established."
  [client-site-id]
  (:instructions
   (record-to-map (AwesomeCoreHandler/getClientSite client-site-id))))

(defn get-starting-checklist
  "Returns the starting checklist for the couriers.
  Throws an IllegalStateException if no connection to the database
  is already established."
  []
  (AwesomeCoreHandler/getStartingList))

(defn get-ending-checklist
  "Returns the ending checklist for the couriers.
  Throws an IllegalStateException if no connection to the database
  is already established."
  []
  (AwesomeCoreHandler/getEndingList))

;;; Altering the database

(defn add-shipping-info
  "Adds the given information as a new item awaiting to be picked up
  and shipped by the couriers. Returns true on success.

  barcode is an int
  reqs/description/username are strings
  items is a map from String -> String (name -> capColor)
  destination-id is an int

  Throws an IllegalStateException if no connection to the database
  is already established."
  [barcode reqs description items username destination-id]
  (try
    (AwesomeCoreHandler/addShippingInformation
     barcode description reqs items username destination-id)
    (catch IllegalArgumentException exn
      false)))

(defn add-client-site
  "Adds the given information as a new client site location.  Returns
  true on success.

  Throws an IllegalStateException if no connection to the database is
  already established."
  [name hospital-id instructions phone]
  (try
    (AwesomeCoreHandler/addClientSite
     name hospital-id instructions phone)
    (catch IllegalArgumentException exn
      false)))

(defn add-hospital
  "Adds the given information as a new hospital.  Returns true on
  success.

  Throws an IllegalStateException if no connection to the database is
  already established."
  [name address]
  (AwesomeCoreHandler/addHospital name address))

(defn add-user
  "Adds the given information as a user.  Handles all user types
  including couriers.  Returns true on success.

  Throws an IllegalStateException if no connection to the database is
  already established."
  [username password first-name last-name
  site-id type]
  (try
    (AwesomeCoreHandler/addUser
     username password first-name last-name site-id type)
    (catch IllegalArgumentException exn
      false)))

(defn set-route
  "Sets the route for the given courier username.  Returns true on
  success.

  Throws an IllegalStateException if no connection to the database is
  already established."
  [username route]
  (AwesomeCoreHandler/changeCourierRoute username route))

(defn set-starting-checklist
  "Sets the starting checklist for couriers.  Returns true on success.

  Throws an IllegalStateException if no connection to the database is
  already established."
  [list]
  (AwesomeCoreHandler/changeStartingList list))

(defn set-ending-checklist
  "Sets the ending checklist for couriers.  Returns true on success.

  Throws an IllegalStateException if no connection to the database is
  already established."
  [list]
  (AwesomeCoreHandler/changeEndingList list))

(defn check-in-specimen
  "Sets the status for the given barcoded specimen to be \"Arrived\"
  Returns true on success.

  Throws an IllegalStateException if no connection to the database is
  already established."
  [barcode]
  (AwesomeCoreHandler/arrived barcode))